import scipy
import copy
import random
from helper_codes.general_stimulus import general_stimulus, GeneralStim

def single_step_scanning_dot_settings():
    '''
    This function tells you what settings are needed and in what order for this stimulus.
    Inputs:
        None
    Returns:
        settings        : A list of tupples.  In each tupple is
                            (setting_name, setting_description, default_value).
                          The list is of length num_settings + 1, so that the final
                             value in the list is True/False wether or not the stimulus
                             can be triggered via the parallel port or not.
    '''
    settings = []
    settings.append( ('num_scans',
                     'The number of times the dot would scan across the RF',
                     5.0 ))
    settings.append( ('scan_number',
                     'The scan row which will actually occur. (indexed from one)',
                     1   ))
    settings.append( ('dot_size',
                     'The size of the moving dot. (normalized RF units)',
                     0.2 ))
    settings.append( ('angle',
                     'The angle describing where the dot will start moving from. (degrees)',
                     0 ))
    settings.append( ('duration',
                     'The amount of time the stimulus will move. (seconds)',
                     3.5 ))
    settings.append( ('b_wait_time',
                     'The amount of time before stimulus movement. (seconds)',
                     2.0 ))
    settings.append( ('a_wait_time',
                     'The amount of time after stimulus movement. (seconds)',
                     2.0 ))
    settings.append( ('shade',
                     '1.0 for black dots, -1.0 for white dots.',
                     1.0 ))
    settings.append(True) # this stimulus can be triggered
    return settings



def single_step_scanning_dot_runtime(num_scans, scan_number, dot_size, angle, 
                              duration, b_wait_time, a_wait_time, 
                              shade):
    """
    Calculate and return the number of seconds that this stimulus will need to run, after being triggered.
    """
    return (duration)*1.0 + b_wait_time + a_wait_time + 0.99 # 0.99 added so rounding is always up.

class Stim(GeneralStim):
    """
    This is a basic stimulus object, it must have:
        .setup(window) method to set itself up to be drawn
        .draw() method to draw itself to the display
        .evolve() method to set itself up for the next draw
        .exhausted variable set to True after stim is done.
    """
    def __init__( self, num_scans, scan_number, dot_size, angle, duration, b_wait_time, 
                  a_wait_time, shade, 
                  refresh_rate, preview, save_frames):
        if num_scans < 2.0:
            num_scans = 2
        else:
            num_scans = int(num_scans)
        self.num_scans = num_scans
        self.scan_number = scan_number
        self.dot_size = dot_size
        # to get the angle to be positive if clockwise
        self.angle = -angle
        self.duration = duration
        self.b_wait_time = b_wait_time
        self.a_wait_time = a_wait_time
        self.shade = shade
        self.refresh_rate = refresh_rate
        self.preview = preview
        self.save_frames = save_frames

        if scan_number < 1:
            scan_number = 1
        if scan_number > num_scans:
            scan_number = num_scans
        rows = [scan_number-1]
        self.rows = rows
        self.exhausted = False
        self.times_evolved = 0

    def setup(self, window):
        from psychopy import visual

        def find_positions(row, num_rows, box_size, angle, move_frames): 
            """
            Returns the positions of point moving from one side of an oriented 
                box to the other side.
            """
            import scipy

            # find the x,y coordinates as if there was no angle, then we'll rotate every point
            unr_x = scipy.linspace(-box_size/2.0, box_size/2.0, move_frames)
            unr_y = [box_size/2.0 - box_size*(row/((num_rows-1)*1.0)) for i in xrange(move_frames)]

            # rotate the coordinates
            angle_rad = angle*scipy.pi/180.0
            rot_matrix = [[scipy.cos(angle_rad), scipy.cos(angle_rad+scipy.pi/2.0)],
                          [scipy.cos(scipy.pi/2.0-angle_rad), scipy.cos(angle_rad)]]
            rot_matrix = scipy.array(rot_matrix)
            
            positions = []
            for ux, uy in zip(unr_x,unr_y):
                positions.append(scipy.dot( scipy.array([ux,uy]), rot_matrix) )

            return positions


        # determine the number of frames per scan.
        move_frames   = int(self.duration*self.refresh_rate)
        b_wait_frames = int(self.b_wait_time*self.refresh_rate)
        a_wait_frames = int(self.a_wait_time*self.refresh_rate)

        # determine the size of the box
        self.box_size = 1.0

        all_positions = []
        for index, row in enumerate(self.rows):
            positions      = []
            m_positions = find_positions(row, self.num_scans, self.box_size, 
                                         self.angle, move_frames) 
            last_pos = copy.deepcopy(m_positions[-1])
            first_pos = copy.deepcopy(m_positions[0])
            # add in enough frames to account for the wait before movement.
            positions.extend(   [first_pos for i in xrange(b_wait_frames)])
            positions.extend(    copy.deepcopy(m_positions)    )
            # add in enough frames to account for the wait after each movement.
            positions.extend(   [last_pos for i in xrange(a_wait_frames)])
            all_positions.append(positions)
        self.positions    = all_positions

        # construct the patch_info for the dot
        texture = scipy.ones((4,4),dtype=scipy.float64)*-1.0*self.shade
        dots = []
        for index, row in enumerate(self.rows):
            dots.append( visual.PatchStim( window, 
                                         tex=texture, 
                                         pos = self.positions[index][0],
                                         size=(self.dot_size, self.dot_size), 
                                         mask='circle',
                                         units='norm') )
        self.dots = dots
        self.textures = self.dots


    def draw(self):
        for dot in self.dots:
            dot.draw()

    def evolve(self):
        self.times_evolved += 1

        dot_i = min(self.times_evolved, len(self.positions[0]))

        for index, dot in enumerate(self.dots):
            dot.setPos(self.positions[index][dot_i])
            
        if self.times_evolved >= len(self.positions[0])-1:
            if (not self.preview) or self.save_frames:
                self.exhausted = True
            else:
                self.times_evolved = 0 # continue looping forever in preview mode.

def single_step_scanning_dot(num_scans, scan_number, dot_size, angle, 
                      duration, b_wait_time, a_wait_time, 
                      shade,
                      win_size=[1440, 900], refresh_rate=60, 
                         window=None,
                         eye_cup=None, 
                         rf=None, 
                         save_frames=False,
                         save_frames_factor=1.0,
                         save_frames_size=100,
                         display_saved_frames=False,
                         preview=True):
    """
    This stimulus shows a set of dots moving from one side of a box to the other through the
        receptive field.
    Inputs:
        num_scans       : The number of dots that will start on one side and end at the other side of the rf
        scan_number     : The scan row which will actually occur. (indexed from one)
        dot_size        : The diameter of the moving dot (in normalized RF units)
        angle           : the orientation angle of the box
        duration        : the time where the dot will be moving
        b_wait_time     : the time before movement begins for the first time
        a_wait_time     : the time after movement ends for the last time
        shade           : +1 for black dots -1 for white
        --- kwargs --
        window          : a psychopy.visual Window object, if not passed it will be constructed.
                        :    if passed it will not be closed (a blank screen will be drawn)
        preview         : If false the program will wait for a pulse trigger, otherwise it won't.
        save_frames_factor  : if None, then no frames are saved, otherwise it is a 
                                scaling factor so we can scale down the frames and
                                output a frames.gif (an animated gif file).
        eye_cup         : an Eye_cup object, see its docstring for more info.
        rf              : RF object, to set up new coordinate system.
    Returns:
        times     : The times when frames were shown, with zero being the first frame time.
    """
    stim_list = [ Stim( num_scans, scan_number, dot_size, angle, duration, b_wait_time, 
                        a_wait_time, shade, 
                        refresh_rate, preview, save_frames) ]
    return_values = general_stimulus( stim_list, shade, win_size, 
                                         window=window,
                                         eye_cup=eye_cup, 
                                         rf=rf, 
                                         save_frames=save_frames,
                                         save_frames_factor=save_frames_factor,
                                         save_frames_size=save_frames_size,
                                         display_saved_frames=display_saved_frames,
                                         preview=preview)
    return return_values


if __name__ == '__main__':
    # ---------------------------------
    # -- Set the stimulus parameters --
    # ---------------------------------
    num_scans = 12
    scan_number = 2
    dot_size = 0.2
    angle = 90
    duration = 0.2
    b_wait_time = 1.0
    a_wait_time = 1.0
    shade = -1.0

    return_times = single_step_scanning_dot(num_scans, scan_number, dot_size, angle, 
                      duration, b_wait_time, a_wait_time, 
                      shade)


